Cats Effect IO.fromFutureの2.0.0での変更について
はじめに
今更ですがメインのプロジェクトでCats Effectのバージョンを2.x.xに上げたのでAPIの変更点の対応を行っていました。今回はIO#fromFutureが変更されたことについて詳細を調べてみました。
IO#fromFutureの変更点
以下のようにfromFutureがContextShiftを要求するようになって、これまでのインターフェースはdeprecatedになっています。
def fromFuture[A](iof: IO[Future[A]])(implicit cs: ContextShift[IO]): IO[A] = iof.flatMap(IOFromFuture.apply).guarantee(cs.shift) @deprecated("Use the variant that takes an implicit ContextShift.", "2.0.0") private[IO] def fromFuture[A](iof: IO[Future[A]]): IO[A] = iof.flatMap(IOFromFuture.apply)
何が変わったかというとIO#gurantee(ContextShift)を呼ぶようになったのですが、これにはどういった意味があるのでしょう?
IO#guranteeとは何か
guranteeを見ると IO.unit.bracket(_ => io)(_ => f)
と同等だと説明しています。guranteeはIOの実行が成功、失敗、キャンセルのいずれの場合でもfinalizerを実行するIOを返します。(Java でいうtry-catch-finallyのようなものですが、finalize処理を含めて合成可能なのです。最高だな。)
/** * Executes the given `finalizer` when the source is finished, * either in success or in error, or if canceled. * * This variant of [[guaranteeCase]] evaluates the given `finalizer` * regardless of how the source gets terminated: * * - normal completion * - completion in error * - cancelation * * This equivalence always holds: * * {{{ * io.guarantee(f) <-> IO.unit.bracket(_ => io)(_ => f) * }}} * * As best practice, it's not a good idea to release resources * via `guaranteeCase` in polymorphic code. Prefer [[bracket]] * for the acquisition and release of resources. * * @see [[guaranteeCase]] for the version that can discriminate * between termination conditions * * @see [[bracket]] for the more general operation */ def guarantee(finalizer: IO[Unit]): IO[A] = guaranteeCase(_ => finalizer)
再びfromFutureへ
fromFutureはFutureから生成したIOのfinalizerでコンテキストシフトをしています。
ということはgurannteeなしの場合はFutureのあとのIOはFutureが実行されたExecutionContextで実行されていたのか?ということで確かめてみます。
package example import cats.effect.{ExitCode, IO, IOApp, Resource} import java.util.concurrent.Executors import scala.concurrent.{ExecutionContext, Future} object FromFutureExample extends IOApp { override def run(args: List[String]): IO[ExitCode] = //区別しやすくするためにFutureはスレッドプール で実行 Resource .make(IO(Executors.newFixedThreadPool(1)))(es => IO(es.shutdownNow())) .map(es => ExecutionContext.fromExecutor(es)) .use { implicit ec => for { _ <- IO(println( s"threadName of before_future: ${Thread.currentThread().getName}")) _ <- IO.fromFuture( IO(Future(println( s"threadName of future: ${Thread.currentThread().getName}")))) _ <- IO( println( s"threadName of after_future: ${Thread.currentThread().getName}")) } yield ExitCode.Success } }
実行結果は以下のようになります(cats effect 1.4.0で実行)
threadName of before_future: ioapp-compute-0 threadName of future: pool-1-thread-1 threadName of after_future: ioapp-compute-1
次に2.x系と同様にguranteeを追加して実行します。
for式だけ抜粋するとこうなります。
for { _ <- IO(println( s"threadName of before_future: ${Thread.currentThread().getName}")) _ <- IO .fromFuture(IO(Future(println( s"threadName of future: ${Thread.currentThread().getName}")))) .guarantee(contextShift.shift) _ <- IO( println( s"threadName of after_future: ${Thread.currentThread().getName}")) } yield ExitCode.Success
この実行結果は以下の通り(2.x相当)
threadName of before_future: scala-execution-context-global-11 threadName of future: pool-1-thread-1 threadName of after_future: scala-execution-context-global-11
やはりFutureの処理の前後でExecutionContextの切り替えが強制されるかどうかが違いのようです。
変更の経緯
この変更の経緯はcats-effect #546 で確認できます。破壊的な変更だと分かりながらもExecutionContextが切り替わったことに気付きづらいそれまでのインターフェースよりはいいということで変更されたようです。
まとめ
今更2.x系の変更点でしたが、今回移行してみてインタフェースが変わったこと以上に、これまでExecutionContextが変わったことに気づかず使っていた点にショックを受けました。